package org.flyby.api.daoImpl;

import org.apache.log4j.Logger;
import org.flyby.api.dao.BaseCommonDao;
import org.flyby.api.persistence.BaseEntity;
import org.hibernate.Session;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.*;
import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

@Repository
@Transactional
public abstract class BaseCommonDaoImpl<E extends BaseEntity> implements BaseCommonDao<E> {

    private static final Logger log = Logger.getLogger(BaseCommonDaoImpl.class);
    @PersistenceContext
    protected EntityManager em;
    private Class<E> type;

    protected BaseCommonDaoImpl() {
        Type t = getClass().getGenericSuperclass();
        ParameterizedType pt = (ParameterizedType) t;
        type = (Class) pt.getActualTypeArguments()[0];
    }

    @Override
    public E save(final E entity) {
        E persistentEntity = null;
        boolean saved = false;

        if (!em.contains(entity)) {
            int id = entity.getId();
            if (id != 0) {
                persistentEntity = em.merge(entity);
                saved = true;
            }
        }
        if (!saved) {
            em.persist(entity);
            persistentEntity = entity;
        }
        return persistentEntity;
    }

    @Override
    public E save(final E entity, boolean refresh) {
        save(entity);
        flush();
        em.refresh(entity);

        return entity;
    }

    private void remove(E entity, boolean needsFlush) {
        if (!em.contains(entity)) {
            entity = em.merge(entity);
        }
        em.remove(entity);
        if (needsFlush) {
            flush();
        }
    }

    @Override
    public void remove(final E entity) {
        remove(entity, true);
    }

    @Override
    public void removeAll(final Collection<E> entities) {
        for (E entity : entities) {
            remove(entity, false);
        }
        flush();
    }

    protected void flush() {
        em.flush();
    }

    protected void flushAndClear() {
        em.flush();
        em.clear();
    }

    @Override
    public E findById(Serializable id) {
        return em.find(type, id);
    }

    @Override
    public E update(final E entity) {
        E updatedEntity = em.merge(entity);
        em.flush();

        return updatedEntity;
    }

    public E executeSingleResultNamedQuery(String namedQuery) {
        return executeSingleResultNamedQuery(namedQuery, null);
    }

    /**
     * @param namedQuery namedQuery
     * @param parameters parameters
     * @return The single result Entity
     * @throws javax.persistence.NonUniqueResultException
     *          if more than one result is found
     */
    public E executeSingleResultNamedQuery(String namedQuery, Map<String, Object> parameters) {
        Query query = prepareQuery(namedQuery, parameters);

        // Return the single result
        try {
            return (E) query.getSingleResult();
        } catch (NoResultException e) {
            return null;
        } catch (EntityNotFoundException e) {
            return null;
        } catch (IllegalStateException x) {
            log.error(x);
            return null;
        }
    }

    public E executeFirstResultNamedQuery(String namedQuery, Map<String, Object> parameters) {
        Query query = prepareQuery(namedQuery, parameters);
        query.setFirstResult(0);
        query.setMaxResults(1);

        // Return first result
        try {
            return (E) query.getSingleResult();
        } catch (NoResultException nre) {
            return null;
        } catch (Exception e) {
            log.error(e);
            return null;
        }
    }

    /**
     * Creates a Query Object to be used to execute queries.
     *
     * @param namedQuery the named query
     * @param parameters query parameters. May be 'null'
     * @return Query Object
     */
    private Query prepareQuery(String namedQuery, Map<String, Object> parameters) {
        em.setFlushMode(FlushModeType.COMMIT);

        Query query = em.createNamedQuery(namedQuery);

        // Replace the parameters place holders
        if (parameters != null) {
            for (String key : parameters.keySet()) {
                query.setParameter(key, parameters.get(key));
            }
        }
        return query;
    }

    protected Object executeFirstResultObjectNamedQuery(String namedQuery, Map<String, Object> parameters) {
        Query query = prepareQuery(namedQuery, parameters);
        query.setFirstResult(0);
        query.setMaxResults(1);

        try {
            List<Object[]> results = query.getResultList();
            if (results != null && !results.isEmpty()) {
                return results.get(0);
            } else {
                return null;
            }
        } catch (Exception e) {
            log.error("Failed execute first-result-object-named-query: " + namedQuery, e);
            return null;
        }
    }

    // Returns the number of entities updated or deleted
    protected int executeUpdateNamedQuery(String namedQuery, Map<String, Object> parameters) {
        Query query = prepareQuery(namedQuery, parameters);

        try {
            return query.executeUpdate();
        } catch (Exception e) {
            String errMsg = "Failed executing update query. namedQuery: " + namedQuery;
            log.error(errMsg);
            throw new RuntimeException(errMsg, e);
        }
    }

    public List<E> executeMultiResultNamedQuery(String namedQuery) {
        Query query = prepareQuery(namedQuery, null);
        return query.getResultList();
    }

    public List<E> executeMultiResultNamedQuery(String namedQuery, Map<String, Object> parameters) {
        em.setFlushMode(FlushModeType.COMMIT);
        Query query = prepareQuery(namedQuery, parameters);
        return query.getResultList();
    }

    /**
     * For executing queries that return a list of Entities with ** pagination **
     * You can specify bounds upon your result set (the maximum number of rows you want to retrieve and / or
     * the first row you want to retrieve)
     *
     * @param namedQuery  named-query
     * @param parameters  query parameters (may be null)
     * @param firstResult the first result to retrieve, numbered from <tt>0</tt>
     * @param maxResults  the maximum number of results
     * @return array of type Object
     */
    protected List<E> executeMultiResultNamedQuery(String namedQuery,
                                                   Map<String, Object> parameters,
                                                   int firstResult,
                                                   int maxResults) {
        Query query = prepareQuery(namedQuery, parameters);
        query.setFirstResult(firstResult);
        query.setMaxResults(maxResults);

        return query.getResultList();
    }

    /**
     * for queries that return a list of objects that are not subclasses of BaseEntity.
     *
     * @param namedQuery named-query
     * @param parameters query parameters
     * @return array of type Object
     */
    protected List<Object> executeObjectsMultiResultNamedQuery(String namedQuery, Map<String, Object> parameters) {
        Query query = prepareQuery(namedQuery, parameters);
        return query.getResultList();
    }

    /**
     * for queries that return a single result of type Object
     *
     * @param namedQuery named-query
     * @param parameters query parameters
     * @return Object the single result
     */
    protected Object executeObjectFirstResultNamedQuery(String namedQuery, Map<String, Object> parameters) {
        Query query = prepareQuery(namedQuery, parameters);
        query.setMaxResults(1);

        try {
            return query.getSingleResult();
        } catch (Exception e) {
            log.info("Failed execute single result query: " + e.getMessage());
            return null;
        }
    }

    /**
     * Executes a count(*) named query.
     *
     * @param countNamedQuery named-query name.   Query must be a count(*) query !
     * @param parameters      query parameters may be null.
     * @return Result of the count operation.
     */
    protected Integer executeCountQuery(String countNamedQuery, Map<String, Object> parameters) throws Exception {
        Query query = prepareQuery(countNamedQuery, parameters);

        try {
            return ((Number) query.getSingleResult()).intValue();
        } catch (NoResultException e) {
            return 0;
        } catch (EntityNotFoundException e) {
            return 0;
        } catch (IllegalStateException x) {
            log.error(x);
            return null;
        } catch (Exception e) {
            log.error("Failed execute count query. Named query: " + countNamedQuery, e);
            throw e;
        }
    }

    /**
     * Executes a max(*) named query.
     *
     * @param maxNamedQuery named-query name.   Query must be a max(*) query !
     * @param parameters    query parameters
     * @return Result of the max operation. 0 if query result is null.
     * @throws RuntimeException if a runtime error occurs
     */
    protected int executeMaxQuery(String maxNamedQuery, Map<String, Object> parameters) {
        return executeMinMaxQuery(maxNamedQuery, parameters);
    }

    /**
     * Executes a min(*) named query.
     *
     * @param minNamedQuery named-query name.   Query must be a min(*) query !
     * @param parameters    query parameters
     * @return Result of the min operation. 0 if query result is null.
     * @throws RuntimeException if a runtime error occurs
     */
    protected int executeMinQuery(String minNamedQuery, Map<String, Object> parameters) {
        return executeMinMaxQuery(minNamedQuery, parameters);
    }

    /**
     * Executes a min(*) or max(*) named query.
     *
     * @param namedQuery named-query name.   Query must be a min(*) query  or max(*) query !
     * @param parameters query parameters
     * @return Result of the min/max operation. 0 if query result is null.
     * @throws RuntimeException if a runtime error occurs
     */
    private int executeMinMaxQuery(String namedQuery, Map<String, Object> parameters) {
        Query query = prepareQuery(namedQuery, parameters);

        try {
            if (query.getSingleResult() == null) {
                return 0;
            }

            return ((Number) query.getSingleResult()).intValue();

        } catch (Exception e) {
            log.error("Failed executing query: " + namedQuery, e);
            throw new RuntimeException("Failed executing query: " + namedQuery, e);
        }
    }

    /**
     * Executes queries which may return multiple objects and/or properties as an array of type Object[]
     *
     * @param namedQuery named-query
     * @param parameters query parameters
     * @return array of type Object[] containing multiple objects(entities) and/or properties
     */
    protected List<Object[]> executeMultipleObjectsQuery(String namedQuery,
                                                         Map<String, Object> parameters) {
        Query query = prepareQuery(namedQuery, parameters);
        return query.getResultList();
    }

    /**
     * Executes queries which may return multiple objects and/or properties as a list of Objects
     *
     * @param namedQuery  named-query
     * @param parameters  query parameters
     * @param firstResult the first result to retrieve, numbered from <tt>0</tt>
     * @param maxResults  the maximum number of results
     * @return array of type Object[] containing multiple objects(entities) and/or properties
     */
    protected List executeMultipleObjectQuery(String namedQuery,
                                              Map<String, Object> parameters,
                                              int firstResult,
                                              int maxResults) {
        Query query = prepareQuery(namedQuery, parameters);
        query.setFirstResult(firstResult);
        query.setMaxResults(maxResults);
        return query.getResultList();
    }

    /**
     * Executes queries which may return multiple objects and/or properties as a list of Objects
     *
     * @param namedQuery named-query
     * @param parameters query parameters
     * @return array of type Object[] containing multiple objects(entities) and/or properties
     */
    protected List executeMultipleObjectQuery(String namedQuery, Map<String, Object> parameters) {
        Query query = prepareQuery(namedQuery, parameters);
        return query.getResultList();
    }

    public List executeNativeSqlQuery(String sql, Map<String, Object> parameters) {
        em.setFlushMode(FlushModeType.COMMIT);
        Query query = em.createNativeQuery(sql);

        if (parameters != null) {
            for (String key : parameters.keySet()) {
                query.setParameter(key, parameters.get(key));
            }
        }

        try {
            return query.getResultList();
        } catch (Exception e) {
            log.error("Failed executing native SQL: " + sql, e);
            return new ArrayList(0);
        }
    }

    public Object executeSingleResultStoredProcedure(String namedQuery, Map<String, Object> parameters) {
        Query query = em.createNamedQuery(namedQuery);

        if (parameters != null) {
            for (String key : parameters.keySet()) {
                query.setParameter(key, parameters.get(key));
            }
        }

        try {
            return query.getResultList().get(0);
        } catch (Exception e) {
            String message = "Failed executing stored procedure: " + namedQuery;
            log.fatal(message);
            throw new RuntimeException(message);
        }
    }

    public List<?> executeMultiResultStoredProcedure(String namedQuery, Map<String, Object> parameters) {
        Query query = em.createNamedQuery(namedQuery);

        if (parameters != null) {
            for (String key : parameters.keySet()) {
                query.setParameter(key, parameters.get(key));
            }
        }

        try {
            return query.getResultList();
        } catch (Exception e) {
            String message = "Failed executing stored procedure: " + namedQuery;
            log.fatal(message);
            throw new RuntimeException(message, e);
        }
    }

    public Integer executeNativeCountSqlQuery(String sql, Map<String, Object> parameters) {
        em.setFlushMode(FlushModeType.COMMIT);
        Query query = em.createNativeQuery(sql);

        if (parameters != null) {
            for (String key : parameters.keySet()) {
                query.setParameter(key, parameters.get(key));
            }
        }

        try {
            return ((Number) query.getSingleResult()).intValue();
        } catch (Exception e) {
            String message = "Failed executing native SQL: " + sql;
            log.fatal(message, e);
            throw new RuntimeException(message, e);
        }
    }

    public int executeUpdateNativeSqlQuery(String sql, Map<String, Object> parameters) {
        em.setFlushMode(FlushModeType.COMMIT);
        Query query = em.createNativeQuery(sql);

        if (parameters != null) {
            for (String key : parameters.keySet()) {
                query.setParameter(key, parameters.get(key));
            }
        }

        try {
            return query.executeUpdate();
        } catch (Exception e) {
            log.error("Failed execute update native SQL: " + sql, e);
            return -1;
        }
    }

    // Evict the entity from the cache
    public <O extends BaseEntity> void evict(O entity) {
        ((Session) em.getDelegate()).evict(entity);
    }

    public BigInteger getNextNumberFromSequence(String sequenceName) {

        String sql = "select " + sequenceName + ".nextval" + " from systables where tabid = 1";

        Query nativeQuery = em.createNativeQuery(sql);

        // Return the single result
        try {
            BigInteger value = (BigInteger) nativeQuery.getSingleResult();
            //return value.longValue();
            return value;
        } catch (Exception e) {
            log.error("Failed retrieving value for sequence: " + sequenceName, e);
            return null;
        }
    }


}
